iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0

上一篇談到 Context Switch 時,我們知道 OS 可以在不同 Process 之間切換。
那這些 Process 到底是怎麼被建立出來的?

在 Unix-like 系統裡,一個非常經典的方式就是:fork();
我們知道 fork() 會複製目前的 Process,建立一個 Child Process。
但「複製 Process」這句話其實很容易讓人誤會。

假設 Parent Process 已經用了 2 GB 記憶體:
Parent Process
Memory = 2 GB

呼叫 fork() 的瞬間,OS 難道真的要立刻:複製 2 GB,再建立 2 GB 嗎?
現代作業系統通常不會這麼做。
所以今天就來拆:

fork() 到底複製了什麼?


1. fork() 做了什麼?

#include <stdio.h>
#include <unistd.h>

int main() {
    fork();

    printf("Hello\n");

    return 0;
}

如果 fork() 成功,執行結果通常會看到兩次:
Hello
Hello
為什麼?

因為原本只有:
Parent Process
呼叫 fork() 之後,系統建立了一個新的 Child Process:

        fork()
          │
    ┌─────┴─────┐
    ▼           ▼
 Parent       Child

而 Parent 和 Child 都會繼續執行 fork() 之後的程式。

所以:
fork();
printf("Hello\n");
printf() 會被兩個 Process 各自執行一次。

fork() 不是讓同一個 Process 執行兩次,而是建立另一個 Process,之後兩個 Process 各自繼續執行。


2. Parent 和 Child 為什麼會從同一個地方繼續?

一個 Process 要能執行,除了程式碼之外,還需要:
Program Counter
Stack Pointer
Registers
Virtual Address Space
Process State
Open Files
...

fork() 建立 Child 時,Child 會從 Parent 的執行環境衍生出自己的初始狀態,因此 fork() 完成之後:
main()


fork()

├──────────────┐
▼ ▼
Parent Child
│ │
▼ ▼
下一行 下一行
它們不是共用同一組 CPU Registers 繼續跑。
Child 是新的 Process,也有自己的執行狀態。
只是建立的那一刻,它的初始 execution context 是從 Parent 衍生而來,所以兩邊才會從相對應的位置繼續。


3. 如果兩邊都從 fork() 回傳值,要怎麼知道誰是誰?

要看 fork() 的 return value,例如:

pid_t pid = fork();

if (pid == 0) {
    printf("I am child\n");
} else {
    printf("I am parent\n");
}

同一次 fork(),Parent 和 Child 看到的 return value 不一樣。

Parent
fork() 回傳 Child PID

Child
fork() 回傳 0

如果建立失敗:fork() 回傳 -1
Parent 和 Child 從幾乎相同的程式位置繼續,但 Kernel 刻意讓它們看到不同的 return value,因此兩邊就可以走不同的程式邏輯。


4. Parent 和 Child 是同一個 Process 嗎?

不是。
Child 是一個新的 Process,因此它有自己的 PID,例如:

Parent PID = 5000
Child PID = 5001

Parent 和 Child 之間會存在親子關係,但:parent ≠ Child
它們是兩個不同的 Process。


5. 那 fork() 到底「複製」了哪些東西?

Process

├── Virtual Address Space
├── CPU Execution State
├── OS Management Information
├── Open Files / Resources
└── Execution Environment

fork() 建立 Child 時,Child 會繼承 Parent 的大量執行環境。

Parent

├── Code
├── Data
├── Heap
├── Stack
├── Open File Descriptors
├── Working Directory
├── Environment
└── Execution State

      fork()
        ↓

Child

├── 對應的 Code / Data / Heap / Stack
├── 繼承的 File Descriptor 狀態
├── 相同的 Working Directory
├── 相同的 Environment
└── 從 Parent 衍生的 Execution State
「Child 看起來得到 Parent 的記憶體內容」不代表 Kernel 一定立刻把所有 Physical Memory 完整複製一份,這就是接下來最重要的 Copy-on-Write。


6. Copy-on-Write 到底是什麼?

Copy-on-Write,簡稱 COW:
先不要複製,真的有人要修改時再複製。
假設 fork() 前:

Parent Virtual Memory


Physical Page A

fork() 之後,不一定馬上變成:Parent → Physical Page A ; Child → Physical Page B

一開始可以先讓 Parent 和 Child 的某些 Virtual Pages 指向相同的 Physical Pages:

Parent Virtual Page ───┐

Physical Page A

Child Virtual Page ────┘

也就是:先共享。
只要兩邊都只是讀取:Parent → read ; Child → read
內容沒有被改變,就沒有必要真的複製。


7.COW 發生 Page Fault 之後呢?

CPU 發現目前的 page mapping 不允許這次 write,因此觸發 Page Fault。在Kernel 接手之後,會判斷:
這不是單純亂存取記憶體,而是合法的 Copy-on-Write 情況。
接著 Kernel 可以:
原本:
Parent ──┐
├── Physical Page A
Child ──┘

1.建立新的 Physical Page後,複製需要的內容:

Physical Page A

│ copy

Physical Page B

2.然後調整 Child 的 mapping:

Parent Virtual Page


Physical Page A

Child Virtual Page


Physical Page B

3.最後 Child 再對自己的 Page B 進行修改。結果:
Parent → 原本內容
Child → 修改後內容

兩邊就真正分開了。

所以 Copy-on-Write 的「Copy」不是:
fork() 發生 → 全部 Copy

而是:
fork() → 先共享

真的有人要 Write
→ Page Fault
→ Kernel 建立 private copy
→ 修改 mapping
→ 再繼續 write
這就是 Copy-on-Write 名字的由來。


8.所以 fork() 後改變變數,另一邊會一起變嗎?

不會。舉個例

#include <stdio.h>
#include <unistd.h>

int main() {
    int x = 10;

    pid_t pid = fork();

    if (pid == 0) {
        x = 20;
        printf("Child: %d\n", x);
    } else {
        x = 30;
        printf("Parent: %d\n", x);
    }

    return 0;
}

Child: 20
Parent: 30
雖然 fork() 剛完成時,兩邊看到的:x = 10 內容相同。

但 Parent 和 Child 有各自的 Virtual Address Space。

即使底層一開始因 COW 暫時共享相同的 Physical Page,一旦其中一方需要修改,Kernel 就會讓它們分離。
因此:
COW 是底層的最佳化,不會改變 fork() 提供給程式的 private address-space


9.Parent 和 Child 的 Virtual Address 可以一樣嗎?

可以。假設:
int x = 10;
Parent 看到:&x = 0x7fff1234

Child 也可能看到:&x = 0x7fff1234

看起來 Address 完全相同,但這是 Virtual Address。


10.既然 Child 是新的 Process,它當然不能什麼都和 Parent 一模一樣。

最明顯的就是:PID
Parent PID ≠ Child PID
Child 必須有自己的 Process Identity。


下一篇:

Day 7|Thread 到底共享什麼、不共享什麼?


上一篇
Day 5|Context Switch:CPU 怎麼從 Process A 切換到 Process B?
下一篇
Day 7|Thread 到底共享什麼、不共享什麼?
系列文
解構作業系統:30 天從 Process、Concurrency 到 Virtual Memory9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言